package nachos.vm;

import java.util.*;

import nachos.machine.*;
import nachos.threads.*;
import nachos.userprog.*;
import nachos.vm.*;

/**
 * A <tt>UserProcess</tt> that supports demand-paging.
 */
public class VMProcess extends UserProcess
{

    /**
     * Allocate a new process.
     */
    public VMProcess()
    {
        super();


    }

    @Override
    public int readVirtualMemory(int vaddr, byte[] data, int offset, int length)
    {
        Lib.assertTrue(Machine.interrupt().disabled());
        Lib.assertTrue(offset >= 0 && length >= 0 && offset + length <= data.length);

        byte[] memory = Machine.processor().getMemory();

        int amount = length; //Math.min(length, memory.length - vaddr);

        for (int i = 0; i < amount; i++)
        {
            int vpn = Processor.pageFromAddress(vaddr + i);
            int voff = Processor.offsetFromAddress(vaddr + i);

            handleTLBMiss(Processor.makeAddress(vpn, voff), lazy);

            Lib.assertTrue(pageTable[vpn].ppn != -1);

            int paddr = Processor.makeAddress(pageTable[vpn].ppn, voff);

            if (paddr < 0 || paddr >= memory.length)
            {
                return 0;
            }

            data[i + offset] = memory[paddr];

            // Set used to true for page
            physPageTable.getEntry(pid, vpn).used = true;
        }


        return amount;
    }

    @Override
    public int writeVirtualMemory(int vaddr, byte[] data, int offset, int length)
    {
        Lib.assertTrue(Machine.interrupt().disabled());
        if (offset < 0 && length < 0 && offset + length < data.length)
        {
            return 0;
        }

        byte[] memory = Machine.processor().getMemory();

        int amount = length; //Math.min(length, memory.length - vaddr);

        for (int i = 0; i < amount; i++)
        {
            int vpn = Processor.pageFromAddress(vaddr + i);
            int voff = Processor.offsetFromAddress(vaddr + i);

            handleTLBMiss(Processor.makeAddress(vpn, voff), lazy);

            Lib.assertTrue(pageTable[vpn].ppn != -1);

            int paddr = Processor.makeAddress(pageTable[vpn].ppn, voff);

            if (paddr < 0 || paddr >= memory.length)
            {
                return 0;
            }

            memory[paddr] = data[i + offset];

            // Set used and dirty to true for page
            physPageTable.getEntry(pid, vpn).used = true;
            physPageTable.getEntry(pid, vpn).dirty = true;
        }

        // TODO: set used and dirty to true for page

        return amount;
    }

    /**
     * Save the state of this process in preparation for a context switch.
     * Called by <tt>UThread.saveState()</tt>.
     */
    @Override
    public void saveState()
    {
        super.saveState();

        //invalidate TLB entries on context switch
        for (int i = 0; i < processor.getTLBSize(); i++)
        {
            processor.writeTLBEntry(i, new TranslationEntry());
        }
    }

    /**
     * Restore the state of this process after a context switch. Called by
     * <tt>UThread.restoreState()</tt>.
     */
    @Override
    public void restoreState()
    {
    }

    /**
     * Initializes page tables for this process so that the executable can be
     * demand-paged.
     *
     * @return	<tt>true</tt> if successful.
     */
    @Override
    protected boolean loadSections()
    {
        // allocate the needed pages
        for (int i = 0; i < numPages; i++)
        {
            //int physPage = pageTable.freePages.removeFirst();
            TranslationEntry t = new TranslationEntry(i, -1, true, false, false, false);
            pageTable[i] = t;
        }

        //everything else is crap
        for (int i = numPages; i < numVirtPages; i++)
        {
            TranslationEntry t = new TranslationEntry(-1, -1, false, false, false, false);
            pageTable[i] = t;
        }

        lazy = new Lazy(coff);

        return true;
    }

    /**
     * Release any resources allocated by <tt>loadSections()</tt>.
     */
    @Override
    protected void unloadSections()
    {
        physPageTable.removeByPid(pid);

        //TODO: Any pages removed from pageTable should also be removed from replacementList
        for (int i = 0; i < processor.getTLBSize(); i++)
        {
            processor.writeTLBEntry(i, new TranslationEntry());
        }
    }

    /*
     * Implements clock algorithm to find page to replace.
     * returns TranslationEntry
     */
    protected int findReplacementPage()
    {

        //return random.nextInt(Machine.processor().getNumPhysPages());

        return clockAlgorithm();

    }

    protected int clockAlgorithm()
    {
        TranslationEntry ti;
        boolean found = false;
        
        Iterator<TranslationEntry> iter = physPageTable.replacementList.iterator();
        
        while (found && iter.hasNext())
        {
            ti = iter.next();
            if (!ti.used)
            {
                //Lib.debug(dbgVM, "Returning: ppn: " + ti.ppn + " vpn: " + ti.vpn);
                physPageTable.replacementList.remove(ti);
                return ti.ppn;
            }
            else
            {
                ti.used = false;
            }
        }
        
        return Lib.random(processor.getNumPhysPages());
    }

    protected int findVictim()
    {
        // Check if any free pages are avyep, ailable in main memory
        // If so, use a free page

        int ppn = findReplacementPage();
        TranslationEntry t = physPageTable.getEntry(ppn);

        //Check if page needs to be written to swap or not
        if (t.dirty)
        {
            // Write page from main memory to swapFile
            if (!swap.out(ppn, physPageTable.getOwnerPid(ppn), t.vpn))
            {
                Lib.debug(dbgProcess, "\tUnsuccessful write to swap file from memory");
                // TODO: probably need to handle this error somehow like kill the process
                // if this error does not halt the system, we may need to add the unsuccessfully
                // written page back into the replacementListIt linked list
                missLock.release();
                handleExit(-1);
            }
            Lib.debug('s', "SWAP OUT!");
        }
        // remove page from pageTable
        physPageTable.removeEntry(PageTable.makeOwner(physPageTable.getOwnerPid(ppn), t.vpn));
        physPageTable.zeroPage(ppn);

        return ppn;
    }

    protected void handleTLBMiss(int badVAddr, Lazy lazy)
    {
        missLock.acquire();
        boolean intStatus = Machine.interrupt().disable();
        //Lib.debug(dbgVM, "MISS pid: " + pid + " vaddr: " + badVAddr);
        int vpn = Processor.pageFromAddress(badVAddr);
        int voff = Processor.offsetFromAddress(badVAddr);
        int ppn = -1;

        TranslationEntry t = physPageTable.getEntry(pid, vpn);

        // page exists in memory
        if (t != null)
        {
            processor.writeTLBEntry(random.nextInt(processor.getTLBSize()), t);
            missLock.release();
            return;
        }
        else
        {
            t = new TranslationEntry(-1, -1, false, false, false, false);
            // Increment page fault counter
            kernel.pageFaultCount += 1;

            // Read in page from swapFile to main memory
            if (swap.contains(pid, vpn))
            {
                Lib.debug('s', "SWAP IN!!");
                ppn = findVictim();
                if (!swap.in(ppn, pid, vpn))
                {
                    missLock.release();
                    handleExit(-1);
                }
            }
            else
            {

                if (lazy.codeCheck(vpn))
                {
                    ppn = findVictim();
                    TranslationEntry code = lazy.loadSection(ppn, vpn);
                    t.readOnly = code.readOnly;
                    t.dirty = true;
                    Lib.debug(dbgVM, "LAZY vpn: " + vpn + " ppn: " + ppn + " pid: " + pid);
                }
                else
                {
                    //just a blank page, nothing to see here, nothing to see
                    ppn = findVictim();
                    Lib.debug(dbgVM, "BLANK vpn: " + vpn + " ppn: " + ppn + " pid: " + pid);
                    t.readOnly = false;
                    t.dirty = true;
                }
            }
        }

        Lib.assertTrue(t != null && ppn != -1);

        t.vpn = vpn;
        t.valid = true;
        t.ppn = ppn;

        TranslationEntry t2 = new TranslationEntry(t);
        physPageTable.addEntry(pid, t2);
        physPageTable.replacementList.add(t2);
        pageTable[vpn] = t;
        processor.writeTLBEntry(random.nextInt(processor.getTLBSize()), t);

        physPageTable.getEntry(t.ppn).dirty = true;

        Machine.interrupt().restore(intStatus);

        missLock.release();
        return;
    }

    /**
     * Handle a user exception. Called by
     * <tt>UserKernel.exceptionHandler()</tt>. The
     * <i>cause</i> argument identifies which exception occurred; see the
     * <tt>Processor.exceptionZZZ</tt> constants.
     *
     * @param	cause	the user exception that occurred.
     */
    public void handleException(int cause)
    {
        boolean intStatus = Machine.interrupt().disable();
        switch (cause)
        {
            case Processor.exceptionTLBMiss:
                handleTLBMiss(Machine.processor().readRegister(Processor.regBadVAddr), lazy);
                //handleTLBMiss(Machine.processor().readRegister(Processor.regBadVAddr), lazy);
                break;
            default:
                super.handleException(cause);
                break;
        }
        Machine.interrupt().restore(intStatus);
    }
    protected static Lock missLock = new Lock();
    protected Lazy lazy = null;
    protected static VMKernel kernel = (VMKernel) VMKernel.kernel;
    protected static PageTable physPageTable = kernel.pageTable;
    protected static Swap swap = kernel.swap;
    protected static Processor processor = Machine.processor();
    protected static Random random = new Random();
    private static final int pageSize = Processor.pageSize;
    private static final char dbgProcess = 'a';
    private static final char dbgVM = 'v';
}
